#include "qmlgraphicsview.h"
#include "qmlgraphicsitem.h"

#include <QCursor>

QmlGraphicsView::QmlGraphicsView(QQuickItem* parent) : QQuickItem(parent)
{
    m_sceneItem = new QQuickItem(this);
    m_sceneItem->setTransformOrigin(TransformOrigin::TopLeft);
    m_sceneItem->setAcceptTouchEvents(true);
    connect(m_sceneItem, &QQuickItem::childrenRectChanged,
            this, [this]()
    {
                if (m_sceneItem != nullptr)
        {
                    const auto cr = m_sceneItem->childrenRect();
                    m_sceneItem->setWidth(cr.width());
                    m_sceneItem->setHeight(cr.height());
        }
    });
    setAcceptedMouseButtons(Qt::RightButton | Qt::LeftButton);
    setTransformOrigin(TransformOrigin::TopLeft);
    setAcceptTouchEvents(true);
}

QmlGraphicsView::~QmlGraphicsView()
{
    disconnect(m_sceneItem, nullptr, this, nullptr);
}

void QmlGraphicsView::setScene(QQuickItem* item)
{
    if (nullptr == item)
    {
        return;
    }
    if (m_sceneItem)
    {
        disconnect(m_sceneItem, nullptr, this, nullptr);
        m_sceneItem->deleteLater();
    }
    item->setParentItem(this);
    m_sceneItem = item;
    emit sceneChanged();
}

void QmlGraphicsView::centerOn(QQuickItem* item)
{
    if (m_sceneItem == nullptr ||
        item == nullptr)
        return;

    const qreal zoom = m_sceneItem->scale();
    const QPointF itemPos = m_sceneItem->mapToItem(item, QPointF{-item->width() / 2.0,
                            -item->height() / 2.0});
    QPointF containerPos{0.0, 0.0};
    containerPos.rx() = itemPos.x() * zoom;
    containerPos.ry() = itemPos.y() * zoom;
    m_sceneItem->setPosition(containerPos);

    const QPointF viewCenter = QPointF{width() / 2.0, height() / 2.0};
    const QPointF viewCenterContainerCs = mapToItem(m_sceneItem, viewCenter);
    const QPointF viewOriginContainerCs = mapToItem(m_sceneItem, QPointF{0, 0});
    const QPointF translationContainerCs = viewCenterContainerCs - viewOriginContainerCs;
    m_sceneItem->setPosition(m_sceneItem->position() + (translationContainerCs * zoom));
}

void QmlGraphicsView::centerOnPosition(QPointF position)
{
    if (m_sceneItem == nullptr)
        return;

    const QPointF QmlGraphicsViewCenter{width() / 2.0, height() / 2.0};
    const QPointF QmlGraphicsViewCenterContainerCs = mapToItem(m_sceneItem, QmlGraphicsViewCenter);
    const QPointF translation{QmlGraphicsViewCenterContainerCs - position};

    const qreal zoom = m_sceneItem->scale();
    m_sceneItem->setPosition(m_sceneItem->position() + (translation * zoom));
}

void QmlGraphicsView::moveTo(QPointF position)
{
    if (m_sceneItem == nullptr)
        return;
    const qreal zoom = m_sceneItem->scale();
    m_sceneItem->setPosition(-position * zoom);
}

void QmlGraphicsView::fitContentInView(qreal forceWidth, qreal forceHeight)
{
    if (m_sceneItem == nullptr)
        return;

    QRectF rect;
    foreach (const auto &item, m_sceneItem->childItems())
    {
        rect |= QRectF(item->position(), item->size());
    }

    if (!rect.isEmpty())
    {
        const qreal viewWidth = forceWidth > 0.0 ? forceWidth : width();
        const qreal viewHeight = forceHeight > 0.0 ? forceHeight : height();

        const qreal fitWidthZoom = viewWidth / rect.width();
        const qreal fitHeightZoom = viewHeight / rect.height();

        qreal fitZoom = fitWidthZoom;
        if (rect.height() * fitWidthZoom > viewHeight)
            fitZoom = fitHeightZoom;
        if (qFuzzyCompare(1.0 + qAbs(fitZoom), 1.0))
            return;
        if (fitZoom > 0.999999)
            fitZoom = 1.0;

        m_zoom = fitZoom;
        m_sceneItem->setScale(m_zoom);
        centerOnPosition(rect.center());
        emit zoomChanged();
        emit sceneItemModified();
        sceneModified();
    }
}

QmlGraphicsItem* QmlGraphicsView::itemAt(const QPoint &pos) const
{
    auto items = itemsAt(pos);
    return items.isEmpty() ? nullptr : items.first();
}

QList<QmlGraphicsItem*> QmlGraphicsView::itemsAt(const QPoint &pos) const
{
    QList<QmlGraphicsItem*> items;
    auto point = mapToItem(m_sceneItem, pos);
    foreach (QQuickItem* item, m_sceneItem->childItems())
    {
        auto graphicsItem = qobject_cast<QmlGraphicsItem*>(item);
        if (nullptr == graphicsItem)
        {
            continue;
        }
        auto rect = QRectF(item->position(), item->size());
        if (rect.contains(point))
        {
            items.append(graphicsItem);
        }
    }
    return items;
}

QList<QmlGraphicsItem*> QmlGraphicsView::items() const
{
    QList<QmlGraphicsItem*> items;
    foreach (QQuickItem* item, m_sceneItem->childItems())
    {
        auto graphicsItem = qobject_cast<QmlGraphicsItem*>(item);
        if (graphicsItem)
        {
            items.append(graphicsItem);
        }
    }
    return items;
}

QList<QmlGraphicsItem*> QmlGraphicsView::items(const QRectF &rect, Qt::ItemSelectionMode mode) const
{
    QList<QmlGraphicsItem*> items;
    foreach (QQuickItem* item, m_sceneItem->childItems())
    {
        auto graphicsItem = qobject_cast<QmlGraphicsItem*>(item);
        if (nullptr == graphicsItem)
        {
            continue;
        }
        auto itemRect = QRectF(item->position(), item->size());
        if (rect.intersects(itemRect))
        {
            items.append(graphicsItem);
        }
    }
    return items;
}

void QmlGraphicsView::setZoom(qreal zoom)
{
    if (isValidZoom(zoom))
    {
        switch (m_zoomOrigin)
        {
            case QQuickItem::Center:
            {
                zoomOn(QPointF(width() / 2.0, height() / 2.0), zoom);
            }
            break;
            case QQuickItem::TopLeft:
            default:
            {
                m_zoom = zoom;
                m_sceneItem->setScale(m_zoom);
                emit zoomChanged();
                emit sceneItemModified();
                sceneModified();
            }
        }
    }
}

void QmlGraphicsView::zoomOn(QPointF center, qreal zoom)
{
    const qreal containerCenterX = center.x() - m_sceneItem->x();
    const qreal containerCenterY = center.y() - m_sceneItem->y();
    const qreal lastZoom = m_zoom;

    if (isValidZoom(zoom))
    {
        qreal oldZoomX = containerCenterX * (zoom / lastZoom);
        qreal oldZoomY = containerCenterY * (zoom / lastZoom);

        qreal zoomCorrectionX = containerCenterX - oldZoomX;
        qreal zoomCorrectionY = containerCenterY - oldZoomY;

        m_sceneItem->setX(m_sceneItem->x() + zoomCorrectionX);
        m_sceneItem->setY(m_sceneItem->y() + zoomCorrectionY);
        m_sceneItem->setScale(zoom);
        m_zoom = zoom;
        emit zoomChanged();
        emit sceneItemModified();
        sceneModified();
    }
}

bool QmlGraphicsView::isValidZoom(qreal zoom) const
{
    if (qFuzzyCompare(1.0 + zoom - m_zoom, 1.0))
        return false;
    if ((zoom > m_zoomMin) && (m_zoomMax < 0.0 || zoom < m_zoomMax))
        return true;
    return false;
}

void QmlGraphicsView::setZoomOrigin(QQuickItem::TransformOrigin zoomOrigin)
{
    if (zoomOrigin != m_zoomOrigin)
    {
        m_zoomOrigin = zoomOrigin;
        emit zoomOriginChanged();
    }
}

void QmlGraphicsView::setZoomMax(qreal zoomMax)
{
    if (qFuzzyCompare(1.0 + zoomMax - m_zoomMax, 1.0))
        return;
    m_zoomMax = zoomMax;
    emit zoomMaxChanged();
}

void QmlGraphicsView::setZoomMin(qreal zoomMin)
{
    if (qFuzzyCompare(1.0 + zoomMin - m_zoomMin, 1.0))
        return;
    if (zoomMin < 0.01)
        return;
    m_zoomMin = zoomMin;
    emit zoomMinChanged();
}

void QmlGraphicsView::setDragEnabled(bool dragActive) noexcept
{
    if (dragActive != m_dragActive)
    {
        m_dragActive = dragActive;
        emit dragEnabledChanged();
    }
}

void QmlGraphicsView::setRubberBandDragItem(QQuickItem* rubberBandDragItem)
{
    m_rubberBandDragItem = rubberBandDragItem;
    emit rubberBandDragItemChanged();
}

void QmlGraphicsView::clickEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
    {
        emit clicked(event->localPos());
        if (event->modifiers().testFlag(Qt::ControlModifier))
        {
            graphicsViewCtrlClicked(event->localPos());
        } else
        {
           graphicsViewClicked(event->localPos());
        }
    }
    else if (event->button() == Qt::RightButton)
    {
        emit rightClicked(event->localPos());
        graphicsViewRightClicked(event->localPos());
    }
}

void QmlGraphicsView::resetData()
{
    m_leftButtonPressed = false;
    m_rightButtonPressed = false;
    m_lastPan = QPointF();
    m_mousePressPt = QPointF();
    m_sceneRect = QRectF();
    setCursor(QCursor(Qt::ArrowCursor));
}

void QmlGraphicsView::clearSelectedItems()
{
    foreach (const auto &item, m_selectedItems)
    {
        item->setSelected(false);
    }
    m_selectedItems.clear();
}

void QmlGraphicsView::mousePressEvent(QMouseEvent* event)
{
    if (event->button() == Qt::LeftButton)
    {
        m_leftButtonPressed = true;
        m_lastPan = event->localPos();
        m_mousePressPt = event->localPos();
        event->accept();
        return;
    }
    if (event->button() == Qt::RightButton)
    {
        m_rightButtonPressed = true;
        m_lastPan = event->localPos();
        m_mousePressPt = event->localPos();
        event->accept();
        return;
    }
    event->ignore();
}

void QmlGraphicsView::mouseMoveEvent(QMouseEvent* event)
{
    if (!m_sceneItem)
    {
        QQuickItem::mouseMoveEvent(event);
        return;
    }
    if (m_rightButtonPressed && !m_lastPan.isNull() && m_dragActive)
    {
        setCursor(QCursor(Qt::ClosedHandCursor));
        const QPointF delta = m_lastPan - event->localPos();
        const auto p = QPointF(m_sceneItem->x(), m_sceneItem->y()) - delta;
        m_sceneItem->setX(p.x());
        m_sceneItem->setY(p.y());
        emit sceneItemModified();
        sceneModified();
        m_lastPan = event->localPos();
    }
    QPointF movePt = event->localPos() - m_mousePressPt;
    if (!m_rubberBandDragItem.isNull() && m_leftButtonPressed && (movePt.manhattanLength() >= 5))
    {
        clearSelectedItems();
        const auto &p = event->localPos();
        m_rubberBandDragItem->setX(std::min(p.x(), m_mousePressPt.x()));
        m_rubberBandDragItem->setY(std::min(p.y(), m_mousePressPt.y()));
        m_rubberBandDragItem->setWidth(std::abs(p.x() - m_mousePressPt.x()));
        m_rubberBandDragItem->setHeight(std::abs(p.y() - m_mousePressPt.y()));
        m_sceneRect = mapRectToItem(m_sceneItem,
                                    QRectF(m_rubberBandDragItem->x(), m_rubberBandDragItem->y(),
                                           m_rubberBandDragItem->width(), m_rubberBandDragItem->height()));
        m_rubberBandDragItem->setVisible(true);
        rubberBandDragRectActivated(m_sceneRect);
    }

    QQuickItem::mouseMoveEvent(event);
}

void QmlGraphicsView::mouseReleaseEvent(QMouseEvent* event)
{
    QPointF movePt = event->localPos() - m_mousePressPt;

    if (movePt.manhattanLength() < 5)
    {
        clickEvent(event);
    }
    else
    {
        if (!m_rubberBandDragItem.isNull() && m_leftButtonPressed)
        {
           m_rubberBandDragItem->setVisible(false);
            rubberBandDragRectEnd(m_sceneRect);
        }
    }

    resetData();
    QQuickItem::mouseReleaseEvent(event);
}

void QmlGraphicsView::graphicsViewCtrlClicked(const QPointF &pos)
{
    if (auto item = itemAt(pos.toPoint()))
    {
        bool selected = item->getSelected();
        item->setSelected(!selected);
        if (!selected)
        {
            m_selectedItems.insert(item);
        } else
        {
            m_selectedItems.remove(item);
        }
    }
}

void QmlGraphicsView::graphicsViewClicked(const QPointF &pos)
{
    clearSelectedItems();
    if (auto item = itemAt(pos.toPoint()))
    {
        item->setSelected(true);
        m_selectedItems.insert(item);
    }
}

void QmlGraphicsView::rubberBandDragRectEnd(const QRectF &rect)
{
    auto lists = items(rect);
    foreach (const auto &item, lists)
    {
        item->setSelected(true);
        m_selectedItems.insert(item);
    }
}

void QmlGraphicsView::wheelEvent(QWheelEvent* event)
{
    qreal zoomFactor = (event->angleDelta().y() > 0. ? m_zoomIncrement : -m_zoomIncrement);
#if (QT_VERSION >= QT_VERSION_CHECK(5,14,0))
    zoomOn(event->position(), getZoom() + zoomFactor);
#else
    zoomOn(event->pos(), getZoom() + zoomFactor);
#endif
}
